/* $Id: chat_server.cc jjs $ */
/*
   Copyright (C) 2007 by Jurjen Stellingwerff <jurjen@stwerff.xs4all.nl>
   Part of the Moros Project http://moros.sourceforge.net/

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License.
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY.

   See the COPYING file for more details.
*/
#define DEBUG_ROUTINE "chatserver"

#include <iostream>
#include "../general/communicate.h"
#include "../general/log.h"
#include "../general/arguments.h"
#include "../general/util.h"
#include "world.h"
#include "chatserver.h"
#include <errno.h>
#include <signal.h>

#define CENTRAL_SERVER "127.0.0.1"

#define setsig(c,s) {sigc::signal<void, std::string> s##_signal; \
    s##_signal.connect(sigc::mem_fun(*this, &chatserver::s)); \
    commands[c]=s##_signal;}

chatserver::chatserver() {
    comm=communicate();
    cent_comm=communicate();
    cent_comm.user="central_chat";
    central_server=CENTRAL_SERVER;
	// General options
	setsig("mess", message);
	setsig("channels", list_channels);
	setsig("channel", channel_info);
	setsig("whisper", whisper);
	setsig("join", join_channel);
	setsig("leave", leave);
	setsig("quit", quit);
	// Account commands
	setsig("password", change_password);
	setsig("whispers", whispers);
	setsig("nowhispers", nowhispers);
	setsig("answer", answer);
	setsig("ignore", ignore);
	setsig("unignore", unignore);
	// Moderator commands
	setsig("title", title_channel);
	setsig("invite", invite);
	setsig("kick", kick);
	setsig("mod", mod);
	setsig("mute", mute);
	setsig("voice", voice);
	setsig("silence", silence);
	setsig("unsilence", unsilence);
	// Leader only commands
	setsig("unmod", unmod);
	setsig("type", channel_type);
	setsig("crown", crown);
}

chatserver::~chatserver() {
}

/**
 \todo Cleanup of the room itself after the last person left a room
*/

void chatserver::leadership_check(room *r) {
	std::map<std::string, room_member>::iterator i;
	member *oldest=NULL;
	int otime=INT_MAX;
	if (r->name=="general") { // world channel has no leader
		return;
	}
	for(i=r->members.begin(); i!=r->members.end(); i++) {
		if (i->second.status==Status_leader) return;
		if (i->second.time<otime) {
			oldest=i->second.mem;
			otime=i->second.time;
		}
	}
	if (oldest==NULL) {
		// cleanup channel
	} else {
		r->members[oldest->name].status=Status_leader;
		oldest->ch->write("leader", r->name);
		oldest->ch->send();
	}
}

void chatserver::cleanup(world *w, std::string user) {
	std::map<std::string, room>::iterator i;
	if (w->members.count(user)>0) {
		dbg_calls("Cleanup user %s", user.c_str());
		w->members.erase(user);
	}
	for(i=w->room_names.begin(); i!=w->room_names.end(); i++) {
	    dbg_calls("Cleanup user %s from %s count %d",
            user.c_str(), i->second.name.c_str(), i->second.members.count(user));
		if (i->second.members.count(user)>0) {
			i->second.members.erase(user);
			leadership_check(&i->second);
		}
	}
}

// q: password text

void chatserver::change_password(std::string text) {
	c_member->password = text;
}


/// Let people join a channel

// q: join channel
// s: channel	channel		when a new channel is created


/**
 \todo Passwords on channels.\n
 Let people set a password on a channel, skip the check when people are already member.
*/

void chatserver::join_channel(std::string channelname) {
	room* r=&c_world->room_names[channelname];
	if (r->name=="") {
		c_world->room_names[channelname]=room(channelname);
		c_member->current=&c_world->room_names[channelname];
		c_room=c_member->current;
		room_member m;
		m.mem=c_member;
		if (c_room->name == "general") {
			m.status=Status_member;
		} else {
			m.status=Status_leader;
			c_channel->write("channel", channelname);
            c_channel->write("text", "Created new channel "+channelname);
            c_channel->send();
		}
		m.time=c_time;
		c_room->members[c_member->name]=m;
	} else {
		if (r->type==Room_private) {
			c_channel->write("error", "invitation only");
			return;
		}
		c_member->current=r;
		c_room=c_member->current;
		if (c_room->members.count(c_member->name)>0) {
			return;
		}
		room_member m;
		m.mem=c_member;
		if (c_room->type==Room_invitation) m.status=Status_muted;
		else m.status=Status_member;
		m.time=c_time;
		c_room->members[c_member->name]=m;
		if (c_room->name != c_world->name) {
			c_channel->write("channel", channelname);
			c_channel->write("text", "Joined as "+c_member -> status(c_room));
			if (c_room->description!="") {
                c_channel->write("channel", channelname);
                c_channel->write("text", "Description: "+c_room -> description);
			}
			c_channel->send();
		}
	}
}

// q: text line	a: flooding
// s: mess channel, text line

/// Recieve a message and pass it on to the connected members

/**
 \todo flood control\n
Implement flood control by a test if the same second another message was posted.
 \todo implement a more relaxed way so send messages to clients\n
Do not send data immediatly when messages are distributed but on the moment there was already
contact with the client or after a timeout of a second.
*/
void chatserver::message(std::string text) {
	room_member m=c_room->members[c_member->name];
	if ((m.status==Status_member  && !c_room->silenced) ||
		m.status==Status_moderator ||
		m.status==Status_leader) {
		std::map<std::string, room_member>::iterator i;
		text="<"+c_member->name+"> "+text;
		for(i=c_room->members.begin(); i!=c_room->members.end(); i++) {
			member *m=i->second.mem;
			if (m!=NULL && !m->ignore[c_member->name]) {
				if (m->ch!=NULL) {
					m->ch->write("channel", c_room->name);
					m->ch->write("text", text);
					m->ch->send();
				}
			}
		}
	} else {
		c_channel->write("error", "This channel doesn't accept your input\n");
	}
}

// q: channels		a: channel name, descr title, type type, status (member,muted,moderator,leader)

void chatserver::list_channels(std::string dummp) {
	std::map<std::string, room>::iterator i;
	for(i=c_world->room_names.begin(); i!=c_world->room_names.end(); i++) {
		string status=c_member->status(&(i->second));
		if (status!="" || i->second.type!=Room_private) {
			c_channel->write("channelname", i->second.name);
			c_channel->write("members", i->second.members.size());
			c_channel->write("type", i->second.state());
			if (status != "") c_channel->write("status", status);
			if (i->second.description != "") c_channel->write("descr", i->second.description);
		}
	}
}

// q: channel  channel		a: channel name, descr title, type type, member name, status (member,muted,moderator,leader)

void chatserver::channel_info(std::string channel) {
	if (c_world->room_names.count(channel)==0) {
		c_channel->write("warning", "Unknown channel");
		return;
	}
	room *r=&c_world->room_names[channel];
	string status=c_member->status(r);
	if (status!="" || r->type != Room_private) {
		std::map<std::string, room_member>::iterator i;
		for(i=r->members.begin(); i!=r->members.end(); i++) {
			c_channel->write("member", i->second.mem->name);
			c_channel->write("status", i->second.mem->status(r));
		}
	} else {
		c_channel->write("warning", "Unknown channel");
	}
}

// q: title title   a: error
// s: title channel, text title

void chatserver::title_channel(std::string title) {
	string status=c_member->status(c_room);
	if (status=="leader" || status=="moderator") {
		c_room->description=title.substr(0, 80);
		std::map<std::string, room_member>::iterator i;
		for(i=c_room->members.begin(); i!=c_room->members.end(); i++) {
			member *m=i->second.mem;
			if (!m->ignore[c_member->name]) {
				m->ch->write("title", c_room->name);
				m->ch->write("text", c_room->description);
				m->ch->send();
			}
		}
	} else {
		c_channel->write("error", "You don't have moderation status");
	}
}

// q: quit text		client shuts down, message on channel

void chatserver::quit(std::string message) {
	cleanup(c_world, c_member->name);
	c_channel->finished=true;
}

/// Set the type of a channel

// q: type channel_type, password value

/// \todo add password to channel

void chatserver::channel_type(std::string type) {
	string status=c_member->status(c_room);
	if (status=="leader") {
		if (type=="open") {
			c_room->type=Room_open;
		} else if (type=="invitation") {
			c_room->type=Room_invitation;
		} else if (type=="private") {
			c_room->type=Room_private;
		}
	} else {
		c_channel->write("error", "You don't have leadership status");
	}
}

// q: invite player, channel name

void chatserver::invite(std::string name) {
	string status=c_member->status(c_room);
	if (status=="leader" || status=="moderator") {
		if (c_world->members.count(name)==0) {
			c_channel->write("error", "Unknown user");
			return;
		}
		if (c_room->members.count(name)>0) {
			c_channel->write("warning", "User was alread a member");
			return;
		}
		member *n_member=c_world->members[name];
		if (n_member->ignore[c_member->name]) {
			c_channel->write("error", "Your actions are ignored");
			return;
		}
		room_member m;
		m.mem=n_member;
		m.status=Status_member;
		m.time=c_time;
		c_room->members[name]=m;
		n_member->ch->write("invited", c_room -> name);
		n_member->ch->write("title", c_room -> description);
		n_member->ch->write("status", c_member -> status(c_room));
		n_member->ch->send();
	} else {
		c_channel->write("error", "You don't have moderation status");
	}
}

// q: whisper player, text text

void chatserver::whisper(std::string player) {
	if (c_world->members.count(player)==0) {
		c_channel->write("error", "Unknown user");
		return;
	}
	member *n_member=c_world->members[player];
	if (n_member->ignore[c_member->name]) {
		c_channel->write("error", "Your actions are ignored");
		return;
	}
	if (n_member->autoanswer!="") {
		c_channel->write("answer", n_member->autoanswer);
	}
	if (c_channel->peek("text")) {
		c_channel->next();
		if (!n_member->whispers) return;
		n_member->ch->write("whisper", c_member->name);
		n_member->ch->write("text", c_channel->value);
		n_member->ch->send();
	}
}

// q: leave channel

void chatserver::leave(std::string channel) {
	if (c_world->room_names.count(channel)==0) {
		c_channel->write("error", "Unknown channel");
		return;
	}
	room *n_room=&c_world->room_names[channel];
	n_room->members.erase(c_member->name);
	if (c_member->current->name==channel) {
		join_channel(c_world->name);
	}
	leadership_check(n_room);
}

// q: ignore player

void chatserver::ignore(std::string player) {
	c_member->ignore[player]=true;
}

// q: unignore player

void chatserver::unignore(std::string player) {
	c_member->ignore.erase(player);
}

// q: kick player

void chatserver::kick(std::string player) {
	string status=c_member->status(c_room);
	if (status=="leader" || status=="moderator") {
		if (c_world->members.count(player)==0) {
			c_channel->write("error", "Unknown user");
			return;
		}
		if (c_room->members.count(player)==0) {
			c_channel->write("warning", "User is not a member");
			return;
		}
		if (    c_room->members[player].status==Status_leader ||
                c_room->members[player].status==Status_moderator) {
            c_channel->write("warning", "You cannot kick moderators");
			return;
		}
		member *n_member=c_world->members[player];
		c_room->members.erase(player);
		n_member->ch->write("kicked", c_room->name);
		if (n_member->current->name==c_room->name) {
			n_member->current=&c_world->room_names[c_world->name];
			n_member->ch->write("join",c_world->name);
		}
		n_member->ch->send();
	} else {
		c_channel->write("error", "You don't have moderation status");
	}
}

// q: make player moderator

void chatserver::mod(std::string player) {
	string status=c_member->status(c_room);
	if (status=="leader" || status=="moderator") {
		if (c_world->members.count(player)==0) {
			c_channel->write("error", "Unknown user");
			return;
		}
		if (c_room->members.count(player)==0) {
			c_channel->write("warning", "User is not a member");
			return;
		}
		if (c_room->members[player].status==Status_leader) {
			c_channel->write("warning", "You cannot unthrone leaders");
			return;
		}
		member *n_member=c_world->members[player];
		c_room->members[player].status=Status_moderator;
		n_member->ch->write("moderator", c_room->name);
		n_member->ch->send();
	} else {
		c_channel->write("error", "You don't have moderation status");
	}
}

// q: unmoderate player

void chatserver::unmod(std::string player) {
	string status=c_member->status(c_room);
	if (status=="leader") {
		if (c_world->members.count(player)==0) {
			c_channel->write("error", "Unknown user");
			return;
		}
		if (c_room->members.count(player)==0) {
			c_channel->write("warning", "User is not a member");
			return;
		}
		if (c_room->members[player].status==Status_leader) {
			c_channel->write("warning", "You cannot unthrone leaders");
			return;
		}
		member *n_member=c_world->members[player];
		c_room->members[player].status=Status_member;
		n_member->ch->write("member", c_room->name);
		n_member->ch->send();
	} else {
		c_channel->write("error", "You are not the channel leader");
	}
}

// q: mute	player

void chatserver::mute(std::string player) {
	string status=c_member->status(c_room);
	if (status=="leader" || status=="moderator") {
		if (c_world->members.count(player)==0) {
			c_channel->write("error", "Unknown user");
			return;
		}
		if (c_room->members.count(player)==0) {
			c_channel->write("warning", "User is not a member");
			return;
		}
		if (    c_room->members[player].status==Status_leader ||
                c_room->members[player].status==Status_moderator) {
			c_channel->write("warning", "You cannot mute moderators");
			return;
		}
		member *n_member=c_world->members[player];
		c_room->members[player].status=Status_muted;
		n_member->ch->write("muted", c_room->name);
		n_member->ch->send();
	} else {
		c_channel->write("error", "You don't have moderation status");
	}
}

// q: voice	player

void chatserver::voice(std::string player) {
	string status=c_member->status(c_room);
	if (status=="leader" || status=="moderator") {
		if (c_world->members.count(player)==0) {
			c_channel->write("error", "Unknown user");
			return;
		}
		if (c_room->members.count(player)==0) {
			c_channel->write("warning", "User is not a member");
			return;
		}
		if (c_room->members[player].status!=Status_muted) {
			c_channel->write("warning", "That person had already a voice");
			return;
		}
		member *n_member=c_world->members[player];
		c_room->members[player].status=Status_member;
		n_member->ch->write("voiced", c_room->name);
		n_member->ch->send();
	} else {
		c_channel->write("error", "You don't have moderation status");
	}
}

// q: silence text

void chatserver::silence(std::string text) {
	string status=c_member->status(c_room);
	if (status=="leader" || status=="moderator") {
		c_room->silenced=true;
		std::map<std::string, room_member>::iterator i;
		for(i=c_room->members.begin(); i!=c_room->members.end(); i++) {
			member *m=i->second.mem;
			if (m!=NULL && !m->ignore[c_member->name]) {
				if (m->ch!=NULL) {
					m->ch->write("silence", c_room->name);
					if (text!="") m->ch->write("text", text);
					m->ch->send();
				}
			}
		}
	} else {
		c_channel->write("error", "You don't have moderation status");
	}
}

// q: unsilence text

void chatserver::unsilence(std::string text) {
	string status=c_member->status(c_room);
	if (status=="leader" || status=="moderator") {
		c_room->silenced=false;
		std::map<std::string, room_member>::iterator i;
		for(i=c_room->members.begin(); i!=c_room->members.end(); i++) {
			member *m=i->second.mem;
			if (m!=NULL && !m->ignore[c_member->name]) {
				if (m->ch!=NULL) {
					m->ch->write("unsilence", c_room->name);
					if (text!="") m->ch->write("text", text);
					m->ch->send();
				}
			}
		}
	} else {
		c_channel->write("error", "You don't have moderation status");
	}
}

// q: whispers

void chatserver::whispers(std::string dummy) {
	c_member->whispers=true;
}

// q: nowhispers text

void chatserver::nowhispers(std::string text) {
	c_member->whispers=false;
	c_member->autoanswer=text;
}

// q: answer text

void chatserver::answer(std::string text) {
	c_member->autoanswer=text;
}

// q: crown player

void chatserver::crown(std::string player) {
	string status=c_member->status(c_room);
	if (status=="leader") {
		if (c_world->members.count(player)==0) {
			c_channel->write("error", "Unknown user");
			return;
		}
		if (c_room->members.count(player)==0) {
			c_channel->write("warning", "User is not a member");
			return;
		}
		member *n_member=c_world->members[player];
		c_room->members[player].status=Status_leader;
		c_room->members[c_member->name].status=Status_moderator;
		n_member->ch->write("leader", c_room->name);
		n_member->ch->send();
	} else {
		c_channel->write("error", "You are not the channel leader");
	}
}

/** \todo Check if the current cent_payload = 0 and when not let this new connection wait
*/

void chatserver::handler(channel *ch) {
	if (ch->lock) {
	    if (cent_ch->payload!=0) {
	        ch->write("error", "Sorry currently busy, try again");
	        ch->send();
	        return;
	    }
	    string usern;
	    if (ch->key!="user") {
	        ch->write("error", "Expecting user name");
	        ch->send();
	        return;
	    }
	    usern=ch->value;
        ch->next();
	    c_member=&members[usern];
	    c_member->connected=false;
	    c_member->name=usern;
	    c_member->ch=ch;
	    map<string,world>::iterator i=worlds.begin();
	    c_world=&(i->second); // what to do here?
	    c_member->world=c_world;
	    c_member->current=&c_world->room_names["general"];
	    if (ch->key!="session") {
	        ch->write("error", "Expecting session key");
	        ch->send();
	        return;
	    }
        c_channel=ch;
        cent_ch->payload=ch;
	    cent_ch->write("client", usern);
        cent_ch->write("session", ch->value);
        cent_ch->send();
	    ch->next();
	    ch->payload=c_member;
        ch->lock=false;
	    return;
	} else {
	    c_member=(member*)ch->payload;
	    c_world=c_member->world;
	    c_room=c_member->current;
        c_channel=ch;
	}
	if (!c_member->connected) { // not finished sending session
	    dbg_calls("Waiting on central server");
	    return;
	}
	c_time=time(NULL);
	if (ch->finished) {
		cleanup(c_world, c_member->name);
		return;
	}
	while (true) {
		std::string key=ch->key;
		if (commands.count(key) == 0) {
			dbg_error("Unknown command '%s' skipped", key.c_str());
		} else {
			if (c_member!=NULL && c_room==NULL && key!="join") {
				c_channel->write("error", "please join a room first");
				break;
			}
			if (c_member != NULL && c_room != NULL) {
				std::cout << "User " << c_member->name << " channel " << c_room->name <<
                    " command " << key << " value " << ch->value  << "\n";
			}
			commands[key](ch->value);
		}
		if (!ch->next()) break;
	}
	if (c_member!=NULL) {
		//std::cout << "Finished\n";
		c_member->last=c_time;
	} else {
		dbg_error("No login found");
	}
	c_channel->send();
}

/**
  \todo safer implementation of c_member
*/
void chatserver::client_handler(channel *ch) {
    if (ch->key=="session") {
        c_channel=(channel*)ch->payload;
        c_channel->write("session", ch->value);
        c_channel->send();
        ch->payload=0;
        dbg_calls("Got session from central server for %s", c_member->name.c_str());
   	    join_channel("general");
        c_member->connected=true;
    }
}

bool stopped=false; ///< Is this server asked to stop?

void chatserver::ip_addr(std::string host) {
    central_server=host;
}

void chatserver::session_set(std::string session) {
    cent_comm.session=session;
}

void chatserver::set_name(std::string n) {
    world w;
    w.name=n;
    cent_comm.user=n;
    worlds[n]=w;
}

void chatserver::do_fork(std::string file) {
    dbg_calls("do_fork with pid file '%s'", file.c_str());
    int pid=fork();
    if (pid>0) {
        FILE *fp=fopen(file.c_str(), "w");
        if (!fp) dbg_fatal("Error: %s opening pid file: '%s'", strerror(errno), file.c_str());
        fprintf(fp,"%d\n", pid);
        fclose(fp);
        exit(0);
    }
    fclose(stdin);
    fclose(stdout);
    fclose(stderr);
}

void chatserver::main_loop() {
    if (worlds.empty()) {
        world w;
        w.name="central_chat";
        worlds[w.name]=w;
    }
    sigc::signal<void, channel*> client_handler_signal;
    client_handler_signal.connect(sigc::mem_fun(*this, &chatserver::client_handler));
    cent_ch=cent_comm.client(central_server, 12002, client_handler_signal);
    if (cent_ch==0) dbg_fatal("Cannot connect to central server");
    cent_ch->payload=0;

    sigc::signal<void, channel*> handler_signal;
    handler_signal.connect(sigc::mem_fun(*this, &chatserver::handler));
	comm.server(11001, handler_signal);

	while (!stopped) {
		comm.poll();
		cent_comm.poll();
		usleep(10000);
	}
	dbg_state("Server stopped");
	cout.flush();

}

void sig_int(int signum) {
	dbg_state("Signal interrupt: closing server");
	stopped = true;
}

void sig_pipe(int signum) {
	dbg_state("Signal pipe");
}


/// Set the log level

void chatserver::log_level(std::string level) {
   dbg_level(toint(level), 0);
}

/// Set the log file

void chatserver::log_file(std::string file) {
   dbg_init(file.c_str());
}

#define arg_sig(t,a,s) {sigc::signal<void, std::string> s##_signal; \
    s##_signal.connect(sigc::mem_fun(chatserv, &chatserver::s)); \
    arg_add(t, "", "", a, s##_signal);}

int main(int argc, char* argv[]) {
    chatserver chatserv=chatserver();
	signal(SIGPIPE, sig_pipe);
    signal(SIGINT, sig_int);
    signal(SIGTERM, sig_int);
    dbg_level(DEBUG_CALLS, 0);
    dbg_init("/var/log/chatserver.log");
    dbg_state("Initializing");

    sigc::signal<void, std::string> dummy;
    arg_add(ARG_PROGRAM, "", "", "chatserver", dummy);
    arg_add(ARG_VERSION, "", "", "0.1", dummy);
    arg_add(ARG_DATE, "", "", "April 24 2007", dummy);
    arg_add(ARG_COPYRIGHT, "", "", "Copyright (C) 2007, Jurjen Stellingwerff <jurjen@stwerff.xs4all.nl>", dummy);
    arg_add(ARG_COPYRIGHT, "", "", "This is free software.  You may redistribute copies of it under the terms of", dummy);
    arg_add(ARG_COPYRIGHT, "", "", "the GNU General Public License <http://www.gnu.org/licenses/gpl.html>.", dummy);
    arg_add(ARG_COPYRIGHT, "", "", "There is NO WARRANTY, to the extent permitted by law.", dummy);
    arg_add(ARG_USAGE, "", "", "[OPTION]...", dummy);
    arg_add(ARG_DESCR, "", "*", "Chat server for the moros game.", dummy);
    arg_sig("C", "Central server ip address.", ip_addr);
    arg_extra("central", "HOST");
    arg_sig("S", "Session ID with central server.", session_set);
    arg_extra("session", "ID");
    arg_sig("P", "Fork the process to the background leave the process id in the give pid file.", do_fork);
    arg_extra("pid_file", "FILE");
    arg_sig("N", "Name of this server, default: central_chat", set_name);
    arg_extra("name", "STRING");
    arg_sig("L", "Logfile location, default: /var/log/moros_central.log", log_file);
    arg_extra("log_file", "FILE");
    arg_sig("l", "Loglevel 0-Only errors, 1-state info (default), 2-comm, 3-data, 4-calls", log_level);
    arg_extra("log_level", "LEVEL");
    arg_add(ARG_BUGS, "", "", "info@moroshq.org", dummy);
    arg_parse(argc, argv);

	chatserv.main_loop();
	return 0;
}
